<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>FRU editor</title>
        <!-- <script src="vue.min.js"></script> -->
        <script src="https://unpkg.com/vue"></script>
        <style type="text/css">
            .input-box {
                width: 40em;
                float: left;
            }
            .input-area {
                resize: none;
                width: 100%;
                height: 25em;
                font-family: monospace;
                font-size: 0.9em;
            }
            .output-box {
                width: 40em;
                float: right;
                height: 40em;
                overflow-y: scroll;
            }
            .area-box {
                background-color: #f1f1f1;
                margin-bottom: 0.5em;
                font-family: monospace;
                font-size: 0.9em;
            }
            .area-head {
                padding-top: 0.2em;
                padding-bottom: 0.2em;
            }
            .field-name {
                width: 25em;
                text-align: left;
                padding: 0.2em;
            }
            .field-value {
                width: 25em;
                text-align: right;
                padding: 0.2em;
            }
            pre {
                padding: 0;
                margin: 0;
                font-family: monospace;
                font-size: 0.9em;
            }
            table {
                border-collapse: collapse;
            }
            table, td, th {
                border: 1px solid black;
            }

            .string-input {
                width: 100%;
                margin: 0;
            }
            .transparent {
                filter: alpha(opacity=60);
                -moz-opacity: 0.6;
                -khtml-opacity: 0.6;
                opacity: 0.6;
            }
            .ascii-code {
                resize: none;
                margin-top: 3px;
                margin-bottom: 3px;
                width: 100%;
                height: 3em;
                font-family: monospace;
                font-size: 0.9em;
                border: 0;
            }
            .bg {
                position: absolute;
/*                left: 0.5em;
                top: 0.5em;
*/                
                z-index: -1;

                /*border: 3px;*/
                margin: 3px;
                /*margin: 0.2em;*/
                /*padding: 0.2em;*/
            }
            table.bg {
                /*border: 1px;*/
                /*padding: 0.2em;*/
                /*margin: 0.2em;*/
                width: 100%;
                height: 25em;
                margin: 0;
                padding: 0;
                border: 0;
                background-color: blue;
                border-collapse: collapse;
            }
            table.bg-t, td.bg-t {
                margin: 0;
                padding: 0;
                /*padding-right: 1em;*/
                border: 0;
            }
            .hl {
                background-color: #FCB738;
            }
            input {
                font-family: monospace;
                font-size: 0.9em;
            }
            .toolbar {
                margin-top: 2px;
            }
            .button {
                margin-top: 5px;
                margin-bottom: 5px;
                padding-top: 5px;
                padding-bottom: 5px;
                width: 10em;
            }
            table.file-up-down, table.file-up-down td {
                border: 0;
                margin-top: 0.5em;
                padding: 0.1em;
            }
            .error {
                background-color: #DA5961;
            }
        </style>
    </head>
    <body>
        <div id="app">
            <div class="input-box">
                <div class="bg">
                    <table class="bg-t">
                        <tr v-for="r in raw">
                            <td class="bg-t" v-for="c in r"><pre v-bind:class="{ hl: c.isHl }">{{ c.v }}</pre></td>
                        </tr> 
                    </table>
                </div>
                <textarea class="input-area transparent" id="fru_hex" v-model="fru_hex" placeholder="Please Upload FRU bin file first"></textarea>
                <div class="toolbar">
                    <button class="button" onclick="parse_fru_hex()">Parse</button>
                    <button class="button" onclick="update_checksum()">Update Checksum</button>
                    <input type="text" class="string-input" onkeyup="string2ascii()" v-model="string"/>
                    <textarea class="ascii-code" v-model="ascii_code"></textarea>
                    <div>
                        <table class="file-up-down">
                            <tr>
                            <td><b>Upload FRU bin file</b></td>
                                <td><input type="file" @change="on_file_change"></td>
                            </tr>
                            <tr v-if="new_fru_file">
                                <td><b>Download FRU bin file</b></td>
                                <td><a download="fru.bin" :href="new_fru_file"><button class="button">Download</button></a></td>
                            </tr>
                        </table>
                    </div>
                    <div>
                    </div>
                </div>
            </div>
            <div class="output-box">
                <table class="area-box" v-for="a in fru">
                    <tr><th colspan="2" class="area-head"> {{ a.name }} </th></tr>
                    <tr v-for="f in a.fields" v-on:mouseenter="on_enter_field(f)" v-bind:class="{ hl: f.isHl, error: f.isError}">
                        <td class='field-name'>{{ f.name }}</td>
                        <td class="field-value"><pre>{{ f.value }}</pre></td>
                    </tr>
                </table>
            </div>
        </div>
    </body>
    <script type="text/javascript">
        var fru = [];
        var data = {
            'fru_hex': '',
            'fru': fru,
            'areas': ['i', 'c', 'b', 'p', 'm'],
            'area_offset': {},
            'ascii_code': '',
            'raw': [],
            'hl_bytes': [],
            'hl_field': null,
            'fru_file': null,
            'new_fru_file': null,
            'string': '123abc'
        };
        function on_enter_field(field) {
            var offset;
            if (typeof field.offset == "undefined") {
                return;
            } else {
                offset = field.offset;
            }
            var r = Math.floor(offset / 0x10);
            var c = offset % 0x10;
            var len;
            if (!field.len) {
                len = 1;
            } else {
                len = field.len;
            }
            // select_text(r, c);
            var offset_end = offset + len -1;
            var r_end = Math.floor(offset_end / 0x10);
            var c_end = offset_end % 0x10;
            hl_bg(r, c, r_end, c_end);
            hl_field(field);
        }
        function on_out_field(field) {
            field.isHl = false;
        }
        function on_file_change(e) {
            var files = e.target.files || e.dataTransfer.files;
            if (!files.length) {
                return;
            }
            parse_fru_file(files[0]);
        }
        var app = new Vue({
            el: '#app',
            data: data,
            methods: {
                on_enter_field: on_enter_field,
                on_out_field: on_out_field,
                on_file_change: on_file_change
            }
        });

        // page actions
        function parse_fru_file(file) {
            var reader = new FileReader();
            var vm = app;

            reader.onload = (e) => {
                // todo: http://www.javascripture.com/FileReader  readAsArrayBuffer
                var b = atob(e.target.result.split(';')[1].split(',')[1]);
                var bytes = bin2bytes(b);
                data.fru_file = bytes;
                data.fru_hex = hex_fmt_by_line(bytes, 16);
                parse_fru_hex();
            };
            reader.readAsDataURL(file);
        }
        function save_fru_file() {
            // data:application/octet-stream;base64,AQABBhEAAOcBBRfDICAgwNggICAgICAgICAgIC…AxMjM0NTY3ODkwwN4xMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTDIICAgICAgICDAwa4=
            var bytes = read_fru();
            if (bytes.length < 8) {
                return;
            }
            data.new_fru_file = 'data:application/octet-stream;base64,' + btoa(bytes2bin(bytes));
        }
        function update_checksum() {
            var bytes = read_fru();
            if (bytes.length < 8) {
                return;
            }
            // Common Header {'offset': 0, 'len': 7}
            var c = bytes2hex(checksum(bytes, 0, 7), true, true);
            replace_text(0, 7, c);
            var areas = ['c', 'b', 'p'];
            for (var i=0;i<areas.length;i++) {
                var offset = data.area_offset[areas[i]];
                var len = bytes[offset+1] * 8 -1;
                var c = bytes2hex(checksum(bytes, offset, len), true, true);
                offset += len;
                replace_text(Math.floor(offset / 0x10), offset % 0x10, c);
            }
            parse_fru_hex();
        }
        function set_bg() {
            var bytes = read_fru();
            while (data.raw.length > 0) {
                data.raw.pop();
            }
            for (var i=0;i<bytes.length;i+=16) {
                var r = [];
                // r.push({'isHl': false, 'v': '000000' + bytes2hex(i, true, true) + ':'});
                r.push({'isHl': false, 'v': '         '});
                for (var j=0;j<16;j++) {
                    if (i+j == bytes.length) {
                        break;
                    }
                    var d = {'isHl': false};
                    var v = '  ';
                    // var v = bytes2hex(bytes[i+j], true, true);
                    if (j % 2 == 0) {
                        d.v = ' ' + v;
                    } else {
                        d.v = v;
                    }
                    r.push(d);
                }
                data.raw.push(r);
            }
        }
        function hl_field(field) {
            if (data.hl_field) {
                data.hl_field.isHl = false;
            }
            data.hl_field = field;
            data.hl_field.isHl = true;
        }
        function hl_bg(start_r, start_c, end_r, end_c) {
            while (data.hl_bytes.length > 0) {
                var i = data.hl_bytes.pop();
                data.raw[i[0]][i[1]].isHl = false;
            }
            for (var r=start_r;r<=end_r;r++) {
                var c0 = (r == start_r) ? start_c : 0;
                var c1 = (r == end_r) ? end_c : 15;
                for (var c=c0;c<=c1;c++) {
                    var c2 = c+1;
                    data.raw[r][c2].isHl = true;
                    data.hl_bytes.push([r, c2]);
                }
            }
        }
        function string2ascii() {
            var string = data.string;
            var lst = [];
            for (var i=0;i<string.length;i++) {
                lst.push(bytes2hex(string[i].charCodeAt(), true, true));
                if (i % 2 == 1) {
                    lst.push(' ');
                }
                if (i > 0 && ((i+1) % 16 == 0)) {
                    lst.push('\n');
                }
            }
            data.ascii_code = lst.join('');
        }
        function get_text_offset(text, prefix, r, c) {
            var w = Math.floor(c / 2);
            var i = c % 2;
            var text_len = text.split('\n').slice(0, r).join('\n').length;
            // add a newline at the end if not the first line
            text_len += r > 0 ? 1 : 0;
            var text_offset = text_len + prefix.length + 5 * w + 2 * i;
            return text_offset;
        }
        function select_text(r, c) {
            var fru_hex = document.getElementById('fru_hex');
            var text_offset = get_text_offset(fru_hex.value, '000000d0: ', r, c);
            fru_hex.focus();
            fru_hex.setSelectionRange(text_offset, text_offset+2);
        }
        function replace_text(r, c, s) {
            var v = data.fru_hex;
            var text_offset = get_text_offset(v, '000000d0: ', r, c);
            data.fru_hex = v.slice(0, text_offset) + s + v.slice(text_offset + s.length);
        }
        function parse_fru_hex() {
            var bytes = read_fru();
            if (bytes.length < 8) {
                return;
            }
            while (fru.length > 0) {
                fru.pop();
            }
            fru.push(parse_head(bytes));
            var areas = data['areas'];
            for (var i=0;i<areas.length;i++) {
                if (data['area_offset'][areas[i]] == 0) {
                    continue;
                }
                fru.push(parse_area(bytes, areas[i]));
            }
            set_bg();
            save_fru_file();
        }
        function read_fru() {
            var bytes = [];
            var fru_hex = data.fru_hex.split('\n');
            if (fru_hex[0].length < '00000000: 0100 0106 1100 00e7'.length) {
                alert('Please Upload FRU bin file first');
                return bytes;
            }
            for (var i=0;i<fru_hex.length;i++) {
                if (fru_hex[i].trim().length == 0) {
                    continue;
                }
                var hex = fru_hex[i].split('  ')[0].split(': ')[1].split(' ');
                for (var j=0;j<hex.length;j++) {
                    bytes.push(parseInt(hex[j].substr(0, 2), 16));
                    bytes.push(parseInt(hex[j].substr(2, 4), 16));
                }
            }
            return bytes;
        }

        // parse FRU
        function parse_head(fru_bytes) {
            var head = {'name': 'Common Header', 'fields': []};
            var fields = [
                'Common Header Format Version',
                'Internal Use Area Starting Offset',
                'Chassis Info Area Starting Offset',
                'Board Area Starting Offset',
                'Product Info Area Starting Offset',
                'MultiRecord Area Starting Offset',
                'PAD',
                'Common Header Checksum',      
                ];
            for (var i=0;i<fields.length;i++) {
                var n = fields[i];
                var v = fru_bytes[i];
                var d = {'name': n, 'offset': i};
                if (n.toLocaleLowerCase().endsWith('format version')) {
                    v = bytes2hex(v);
                } else if (n == 'PAD') {
                    v = bytes2hex(v);
                } else if (n.toLocaleLowerCase().endsWith('checksum')) {
                    var exp_cs = checksum(fru_bytes, 0, 7);
                    if (v != exp_cs) {
                        d.isError = true;
                    }
                    v = bytes2hex(v) + '(' + bytes2hex(exp_cs) + ')';
                }
                d.value = v;
                head.fields.push(d);
            }
            var areas = data.areas;
            for (var i=0;i<areas.length;i++) {
                data.area_offset[areas[i]] = fru_bytes[i+1] * 8;
            }
            return head;
        }
        function parse_internal(fru_bytes) {
            var area = {'name': 'Internal Use', 'fields': []};
            area.fields.push({'name': 'Internal Use Format Version', 'value': bytes2hex(fru_bytes[data.area_offset.i]), 'offset': data.area_offset.i});
            area.fields.push({'name': 'Internal use data', 'value': bytes2hex(fru_bytes.slice(data.area_offset.i+1, data.area_offset.c)), 'offset': data.area_offset.i+1});
            return area;
        }
        function parse_area_end(fru_bytes, fields, area_offset, offset, area_end, strings, extra_string, checksum_string) {
            offset = parse_types(fru_bytes, offset, strings, fields);
            var idx = 1;
            while ((fru_bytes[offset] != 0xc1) && (offset < (area_end -1))) {
                offset = parse_type(fru_bytes, offset, extra_string + ' ' + idx, fields);
                idx += 1;
            }
            if (fru_bytes[offset] == 0xc1) {
                fields.push({'name': 'no more info fields', 'value': bytes2hex(fru_bytes[offset]), 'offset': offset});
                offset += 1;
            }
            if (offset < area_end) {
                fields.push({
                    'name': 'any remaining unused space',
                    'value': bytes2hex(fru_bytes.slice(offset, area_end)),
                    'offset': offset,
                    'len': area_end - offset
                });
            }
            
            var cs = fru_bytes[area_end];
            var exp_cs = checksum(fru_bytes, area_offset, area_end - area_offset);
            fields.push({
                'name': checksum_string,
                'value': bytes2hex(cs) + '(' + bytes2hex(exp_cs) + ')',
                'offset': area_end,
                'isError': cs != exp_cs ? true : false
            });
        }
        function parse_chassis(fru_bytes) {
            var area = {'name': 'Chassis Info', 'fields': []};
            var offset = data.area_offset.c;
            var area_end = offset + (fru_bytes[offset+1] * 8) -1;
            var fields = area.fields;
            fields.push({'name': 'Chassis Info Area Format Version', 'value': bytes2hex(fru_bytes[offset]), 'offset': offset});
            fields.push({'name': 'Chassis Info Area Length', 'value': fru_bytes[offset+1], 'offset': offset+1});
            fields.push({'name': 'Chassis Type', 'value': fru_bytes[offset+2], 'offset': offset+2});
            offset += 3;
            var strings = [
                'Chassis Part Number',
                'Chassis Serial Number'
            ];
            parse_area_end(fru_bytes, fields, data.area_offset.c, offset, area_end, strings, 'Custom Chassis Info', 'Chassis Info Checksum');
            return area;
        }
        function parse_board(fru_bytes) {
            var area = {'name': 'Board Info', 'fields': []};
            var offset = data.area_offset.b;
            var area_end = offset + (fru_bytes[offset+1] * 8) -1;
            var fields = area.fields;
            fields.push({'name': 'Board Area Format Version', 'value': bytes2hex(fru_bytes[offset]), 'offset': offset});
            fields.push({'name': 'Board Area Length', 'value': fru_bytes[offset+1], 'offset': offset+1});
            fields.push({'name': 'Language Code', 'value': fru_bytes[offset+2], 'offset': offset+2});
            fields.push({'name': 'Mfg. Date / Time', 'value': bytes2hex(fru_bytes.slice(offset+3, offset+6)), 'offset': offset+3, 'len': 3});
            offset += 6;
            var strings = [
                'Board Manufacturer',
                'Board Product Name',
                'Board Serial Number',
                'Board Part Number',
                'FRU File ID',
            ];
            parse_area_end(fru_bytes, fields, data.area_offset.b, offset, area_end, strings, 'Additional custom Mfg', 'Board Area Checksum');
            return area;
        }
        function parse_product(fru_bytes) {
            var area = {'name': 'Product Info', 'fields': []};
            var offset = data.area_offset.p;
            var area_end = offset + (fru_bytes[offset+1] * 8) -1;
            var fields = area.fields;
            fields.push({'name': 'Product Area Format Version', 'value': bytes2hex(fru_bytes[offset]), 'offset': offset});
            fields.push({'name': 'Product Area Length', 'value': fru_bytes[offset+1], 'offset': offset+1});
            fields.push({'name': 'Language Code', 'value': fru_bytes[offset+2], 'offset': offset+2});
            offset += 3;
            var strings = [
                'Manufacturer Name',
                'Product Name',
                'Product Part/Model Number',
                'Product Version',
                'Product Serial Number',
                'Asset Tag',
                'FRU File ID',
            ];
            parse_area_end(fru_bytes, fields, data.area_offset.p, offset, area_end, strings, 'Custom product info area field', 'Product Info Area Checksum');
            return area;
        }
        function parse_multi(fru_bytes) {
            var area = {'name': 'MultiRecord', 'fields': []};
            return area;
        }
        function parse_area(fru_bytes, area) {
            if (area == 'i') {
                return parse_internal(fru_bytes);
            } else if (area == 'c') {
                return parse_chassis(fru_bytes);
            } else if (area == 'b') {
                return parse_board(fru_bytes);
            } else if (area == 'p') {
                return parse_product(fru_bytes);
            } else if (area == 'm') {
                return parse_multi(fru_bytes);
            }
        }
        function parse_type(fru_bytes, offset, string, fields) {
            var type_len = fru_bytes[offset];
            var type = type_len >> 6;
            var len = type_len & 0x3f;
            var str = bytes2ascii(fru_bytes.slice(offset+1, offset+1+len));
            fields.push({'name': string + ' type/length', 'value': bytes2hex(type_len) + '(' + type + '/' + len + ')', 'offset': offset});
            fields.push({'name': string, 'value': str, 'offset': (len > 0 ? offset+1: offset), 'len': (len > 0 ? len : 1)});
            offset += 1 + len;
            return offset;
        }
        function parse_types(fru_bytes, offset, strings, fields) {
            for (var i=0;i<strings.length;i++) {
                offset = parse_type(fru_bytes, offset, strings[i], fields);
            }
            return offset;
        }
        function checksum(fru_bytes, offset, len) {
            var sum = 0;
            for (var i=0;i<len;i++) {
                sum += fru_bytes[offset];
                offset += 1;
            }
            return (0x100 - (sum % 0x100)) % 0x100;
        }
        function hex_fmt_by_line(bytes, bytes_per_line) {
            var lines = [];
            for (var i=0;i<bytes.length;i+=bytes_per_line) {
                var line = '000000' + bytes2hex(i, true, true) + ':';
                for (var j=0;j<16;j++) {
                    if (i+j == bytes.length) {
                        break;
                    }
                    var v = bytes2hex(bytes[i+j], true, true);
                    if (j % 2 == 0) {
                        v = ' ' + v;
                    }
                    line += v;
                }
                lines.push(line);
            }
            return lines.join('\n');
        }
        function bytes2bin(bytes) {
            var b = '';
            for (var i=0;i<bytes.length;i++) {
                b += String.fromCharCode(bytes[i]);
            }
            return b;
        }
        function bin2bytes(b) {
            var bytes = [];
            for (var i=0;i<b.length;i++) {
                bytes.push(b[i].charCodeAt());
            }
            return bytes;
        }
        function bytes2hex(bytes, noprefix, lowcase) {
            var prefix;
            if (noprefix) {
                prefix = '';
            } else {
                prefix = '0x';
            }
            if (typeof bytes == typeof 1) {
                if (lowcase) {
                    return prefix + (bytes < 16 ? '0' : '') + bytes.toString(16);
                } else {
                    return prefix + (bytes < 16 ? '0' : '') + bytes.toString(16).toUpperCase();
                }
            }
            var hex = [];
            for (var i=0;i<bytes.length;i++) {
                if (lowcase) {
                    hex.push(prefix + (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16));
                } else {
                    hex.push(prefix + (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16).toUpperCase());
                }
            }
            return hex.join(' ');
        }
        function bytes2ascii(bytes) {
            var ascii = '"';
            for (var i=0;i<bytes.length;i++) {
                ascii += String.fromCharCode(bytes[i]);
            }
            ascii += '"';
            return ascii;
        }
        string2ascii();
</script>
</html>